Skip to content

[fix](be) Validate WAL replay column types - #66230

Open
mymeiyi wants to merge 1 commit into
apache:masterfrom
mymeiyi:fix-replay-wal
Open

[fix](be) Validate WAL replay column types#66230
mymeiyi wants to merge 1 commit into
apache:masterfrom
mymeiyi:fix-replay-wal

Conversation

@mymeiyi

@mymeiyi mymeiyi commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem Summary: WAL replay reused a deserialized source column with the target plan's type metadata without validating that the physical and logical types matched. A schema change could therefore expose an INT column as STRING and trigger a fatal cast during projection. Validate the deserialized WAL column, its compatibility with the replay target, and exact logical type equality before inserting it into the destination block. Type mismatches now return a retryable error with WAL and column context instead of reaching the fatal cast.

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: WAL replay reused a deserialized source column with the target plan's type metadata without validating that the physical and logical types matched. A schema change could therefore expose an INT column as STRING and trigger a fatal cast during projection. Validate the deserialized WAL column, its compatibility with the replay target, and exact logical type equality before inserting it into the destination block. Type mismatches now return a retryable error with WAL and column context instead of reaching the fatal cast.

### Release note

None

### Check List (For Author)

- Test: Not run (compilation and tests explicitly excluded by request)
- Behavior changed: Yes (invalid WAL replay type mappings return an error instead of aborting the BE)
- Does this need documentation: No
Copilot AI review requested due to automatic review settings July 29, 2026 08:34
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Improves WAL replay robustness by validating that deserialized WAL columns are physically compatible with the replay target and that logical types match, preventing fatal casts during projection.

Changes:

  • Validate deserialized WAL column integrity via check_type_and_column_match().
  • Validate replay-target compatibility by checking physical column/type match and exact logical type equality.
  • Enrich mismatch errors with WAL path, slot metadata, and source/target column context.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +114
const auto& source_column = src_block.get_by_position(pos);
const auto& target_column = output_block_columns[index];
auto source_status = source_column.check_type_and_column_match();
if (!source_status.ok()) {
return Status::InternalError(
"Invalid WAL column while replaying WAL {}: slot={} (unique_id={}), "
"source_position={}, error={}",
_wal_path, slot_desc->col_name(), slot_desc->col_unique_id(), pos,
source_status.to_string());
}
dst_block.insert(index, ColumnWithTypeAndName(std::move(column_ptr),
output_block_columns[index].type,
output_block_columns[index].name));

ColumnWithTypeAndName replay_column(source_column.column, target_column.type,
target_column.name);
Comment on lines +127 to +135
if (!source_column.type->equals(*target_column.type)) {
return Status::InternalError(
"WAL replay logical type mismatch: wal={}, slot={} (unique_id={}), "
"source_position={}, source_name={}, source_type={}, target_name={}, "
"target_type={}",
_wal_path, slot_desc->col_name(), slot_desc->col_unique_id(), pos,
source_column.name, source_column.type->get_name(), target_column.name,
target_column.type->get_name());
}
Comment on lines +116 to +126
if (!replay_status.ok()) {
return Status::InternalError(
"WAL replay column type mismatch: wal={}, slot={} (unique_id={}), "
"source_position={}, source_name={}, source_type={}, source_column={}, "
"target_name={}, target_type={}, error={}",
_wal_path, slot_desc->col_name(), slot_desc->col_unique_id(), pos,
source_column.name, source_column.type->get_name(),
source_column.column->get_name(), target_column.name,
target_column.type == nullptr ? "null" : target_column.type->get_name(),
replay_status.to_string());
}
@mymeiyi

mymeiyi commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@mymeiyi

mymeiyi commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29461 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 6b2147ec88268362bfd8fc14742621809c14b895, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17699	4058	4055	4055
q2	2109	321	197	197
q3	10195	1395	811	811
q4	4675	470	336	336
q5	7493	851	584	584
q6	185	167	136	136
q7	762	840	612	612
q8	9326	1426	1458	1426
q9	5636	4385	4341	4341
q10	6777	1733	1469	1469
q11	514	355	324	324
q12	726	573	457	457
q13	18095	3434	2713	2713
q14	268	264	248	248
q15	q16	797	772	709	709
q17	1027	934	946	934
q18	7240	5694	5696	5694
q19	1291	1260	1123	1123
q20	759	644	568	568
q21	5858	2584	2433	2433
q22	419	356	291	291
Total cold run time: 101851 ms
Total hot run time: 29461 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4389	4324	4379	4324
q2	300	320	216	216
q3	4615	4982	4466	4466
q4	2094	2149	1364	1364
q5	4379	4290	4285	4285
q6	227	173	127	127
q7	1743	1685	1907	1685
q8	2617	2226	2193	2193
q9	8066	8352	7732	7732
q10	4670	4643	4193	4193
q11	564	418	395	395
q12	744	753	571	571
q13	3306	3659	2932	2932
q14	324	291	279	279
q15	q16	733	744	640	640
q17	1367	1306	1301	1301
q18	8034	7413	7267	7267
q19	1142	1146	1100	1100
q20	2210	2198	1944	1944
q21	5190	4517	4423	4423
q22	515	440	403	403
Total cold run time: 57229 ms
Total hot run time: 51840 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177894 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 6b2147ec88268362bfd8fc14742621809c14b895, data reload: false

query5	4321	636	476	476
query6	464	226	218	218
query7	4865	579	348	348
query8	336	191	173	173
query9	8816	4097	4029	4029
query10	464	345	301	301
query11	5875	2337	2146	2146
query12	157	100	101	100
query13	1268	576	412	412
query14	6212	5190	4850	4850
query14_1	4226	4209	4189	4189
query15	209	198	174	174
query16	996	481	434	434
query17	936	689	561	561
query18	2438	471	356	356
query19	210	191	150	150
query20	111	106	110	106
query21	235	160	135	135
query22	13605	13538	13500	13500
query23	17449	16552	16147	16147
query23_1	16328	16369	16245	16245
query24	7470	1756	1294	1294
query24_1	1302	1292	1288	1288
query25	566	466	389	389
query26	1346	343	217	217
query27	2665	636	390	390
query28	4553	2060	1998	1998
query29	1077	626	500	500
query30	339	252	233	233
query31	1114	1095	988	988
query32	111	67	60	60
query33	540	321	260	260
query34	1232	1110	639	639
query35	768	778	658	658
query36	1033	1025	915	915
query37	155	109	88	88
query38	1872	1712	1667	1667
query39	885	867	849	849
query39_1	829	842	846	842
query40	260	167	148	148
query41	78	69	71	69
query42	101	94	94	94
query43	334	333	286	286
query44	1469	817	791	791
query45	196	183	181	181
query46	1110	1210	759	759
query47	2211	2179	2066	2066
query48	413	421	310	310
query49	594	421	321	321
query50	1056	450	342	342
query51	11129	10951	11048	10951
query52	88	89	76	76
query53	267	286	203	203
query54	309	257	233	233
query55	83	75	70	70
query56	322	313	289	289
query57	1371	1317	1232	1232
query58	322	265	303	265
query59	1560	1674	1387	1387
query60	300	262	253	253
query61	156	144	141	141
query62	556	497	427	427
query63	245	197	198	197
query64	2769	1036	838	838
query65	4688	4654	4652	4652
query66	1851	496	391	391
query67	29332	29200	29119	29119
query68	3220	1560	1037	1037
query69	399	302	269	269
query70	929	840	837	837
query71	373	334	319	319
query72	3049	2722	2346	2346
query73	875	817	414	414
query74	5083	4948	4722	4722
query75	2539	2508	2130	2130
query76	2332	1170	799	799
query77	345	375	265	265
query78	11869	11928	11326	11326
query79	1409	1139	787	787
query80	1250	556	457	457
query81	516	343	286	286
query82	570	160	120	120
query83	416	328	338	328
query84	330	159	130	130
query85	986	607	507	507
query86	405	241	225	225
query87	1832	1824	1737	1737
query88	3744	2799	2765	2765
query89	440	369	335	335
query90	1827	191	196	191
query91	198	192	165	165
query92	65	64	54	54
query93	1556	1512	996	996
query94	675	344	317	317
query95	798	479	459	459
query96	1109	857	367	367
query97	2620	2608	2497	2497
query98	215	208	203	203
query99	1093	1116	966	966
Total cold run time: 263980 ms
Total hot run time: 177894 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.16 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 6b2147ec88268362bfd8fc14742621809c14b895, data reload: false

query1	0.01	0.01	0.00
query2	0.10	0.05	0.04
query3	0.24	0.14	0.13
query4	1.60	0.14	0.14
query5	0.23	0.23	0.22
query6	1.23	1.08	1.12
query7	0.04	0.00	0.00
query8	0.05	0.03	0.04
query9	0.38	0.31	0.32
query10	0.55	0.58	0.58
query11	0.19	0.14	0.14
query12	0.18	0.14	0.14
query13	0.46	0.46	0.47
query14	1.00	1.01	1.01
query15	0.61	0.59	0.59
query16	0.33	0.32	0.31
query17	1.07	1.10	1.08
query18	0.23	0.21	0.21
query19	2.03	1.96	1.98
query20	0.01	0.01	0.01
query21	15.45	0.24	0.15
query22	4.70	0.05	0.05
query23	16.13	0.32	0.12
query24	3.12	0.44	0.32
query25	0.10	0.06	0.06
query26	0.72	0.19	0.15
query27	0.04	0.04	0.04
query28	3.58	0.94	0.57
query29	12.45	4.03	3.28
query30	0.27	0.17	0.16
query31	2.77	0.58	0.31
query32	3.23	0.59	0.49
query33	3.14	3.23	3.19
query34	15.66	4.23	3.56
query35	3.52	3.50	3.49
query36	0.57	0.44	0.41
query37	0.09	0.07	0.06
query38	0.06	0.04	0.04
query39	0.04	0.03	0.03
query40	0.18	0.16	0.15
query41	0.07	0.03	0.03
query42	0.04	0.03	0.03
query43	0.05	0.04	0.03
Total cold run time: 96.52 s
Total hot run time: 25.16 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 0.00% (0/34) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 59.03% (25549/43281)
Line Coverage 43.11% (256140/594162)
Region Coverage 38.78% (202813/523017)
Branch Coverage 40.13% (92514/230521)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes.

Goal and scope: This PR is narrowly focused on preventing a fatal WAL projection cast by validating deserialized source/target columns. The one-file change was reviewed end to end; no additional user focus was provided.

Blocking findings:

  • Supported retained-ID schema evolution can be rejected permanently before the existing load CAST.
  • Distinct AGG_STATE signatures/layouts can pass the new checks and be relabeled.
  • New retryable errors bypass authenticated rollback; wait mode can exhaust retries, move the WAL to tmp, and notify without a failure value.
  • No enabled test exercises the serialized compatibility boundary and transaction/retry lifecycle.

Critical checkpoints:

  • Data correctness/schema compatibility: The blockers above cover retained-ID evolution and parameterized aggregate-state storage. Nullable normalization and stable-ID drop/rename/reorder mapping are otherwise consistent; no WAL serialization format or FE-BE protocol changes.
  • Error handling/lifecycle/transactions: New statuses propagate and InternalError is retried, but authenticated rollback/failure signaling is broken as described inline. No changed status is silently ignored inside WalReader.
  • Concurrency/memory/performance: No new shared state, locks, thread entry, static/global lifetime, material allocation, or row-wise work. Checks are per-column/type-shape.
  • Parallel/config/persistence/observability: Load uses the legacy scanner because v2 is disabled for loads. No new configuration, dynamic-update, EditLog, or storage-format behavior. Both replay-wait settings were audited; contextual errors and existing logs/metrics help, while wait-mode exhaustion signaling is part of MF-1.
  • Tests: No builds or tests were run because this runner is review-only. The repository coverage gap is MF-2.

User focus: No additional focus; the complete PR was reviewed.

Status: Complete review of head 6b2147ec88268362bfd8fc14742621809c14b895; request changes with four inline comments.

auto source_status = source_column.check_type_and_column_match();
if (!source_status.ok()) {
return Status::InternalError(
"Invalid WAL column while replaying WAL {}: slot={} (unique_id={}), "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Add executable coverage for the mismatch-and-retry path

This changes a kernel replay path to replace a fatal projection cast with compatibility checks and retryable statuses, but the PR adds no test for that behavior. The helper test only checks hand-built columns, the WAL writer/reader test round-trips matching types, and the only legacy scanner tests are disabled/count-only. Please add enabled serialized-WAL coverage that distinguishes a supported promotion (which should reach the existing CAST) from a genuinely incompatible mapping and AGG_STATE signature/layout (which should return a contextual error), and verify authenticated rollback, WAL retention/retry, and wait-mode exhaustion signaling after a fragment failure.

return Status::InternalError(
"Invalid WAL column while replaying WAL {}: slot={} (unique_id={}), "
"source_position={}, error={}",
_wal_path, slot_desc->col_name(), slot_desc->col_unique_id(), pos,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Roll back the replay transaction before requeueing this error

These new mismatch returns propagate through the fragment future, but WalTable::_handle_stream_load calls RETURN_IF_ERROR(ctx->load_status_future.get()) before its explicit rollback_txn. This native-insert path gets txn_id from the FE plan and leaves need_rollback false, so neither the callback nor StreamLoadContext's destructor rolls the failed transaction back. Normal mode only attempts an abort at the start of the next retry; with group_commit_wait_replay_wal_finish=true that abort is skipped entirely, and the same label can exhaust its retries before the WAL is moved to tmp and the waiter is notified without a failure value. Please propagate the future failure into both the returned status and ctx->status, restore internal token/auth-code authentication, and complete rollback before requeueing it.


ColumnWithTypeAndName replay_column(source_column.column, target_column.type,
target_column.name);
auto replay_status = replay_column.check_type_and_column_match();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve the serialized source type through compatible schema changes

This target-type validation rejects retained-ID schema changes before the load scanner can cast them. For example, a light ARRAY<INT> -> ARRAY<BIGINT> change can leave an older WAL whose ColumnArray contains Int32; relabeling it as the current ARRAY<BIGINT> fails here even though FileScanner::_cast_to_input_block has the element-wise conversion. Heavy scalar changes are normally fenced by the WAL drain, but its timeout or an unavailable BE can likewise leave an old INT WAL after the current type becomes BIGINT/STRING, and retry will never make it match. Please retain source_column.type and apply a schema-compatibility decision before the existing CAST for supported same-shape conversions; shape evolution such as appended STRUCT fields also needs explicit field/default mapping because the legacy struct CAST requires equal arity.


ColumnWithTypeAndName replay_column(source_column.column, target_column.type,
target_column.name);
auto replay_status = replay_column.check_type_and_column_match();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Validate the complete AGG_STATE signature and storage layout

This compatibility check is too weak for AGG_STATE. If an old sum(INT) WAL survives the bounded schema-change drain after the column becomes avg(BIGINT), FE has accepted a changed AGG_STATE type, but BE DataTypeAggState::equals still compares only the dynamic type and its DataTypeFixedLengthObject checker ignores item_size. The source check, this replay check, and the later equality can therefore accept payloads with different sizes and meanings. The downstream CAST also reduces both states to DataTypeFixedLengthObject and selects the identity path, so sum bytes can be interpreted as an avg state. Please compare the full function/argument/nullability/version signature and validate the serialized layout (including fixed item size) before relabeling the column.

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 0.00% (0/34) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 75.72% (31967/42217)
Line Coverage 60.31% (355929/590198)
Region Coverage 56.83% (298255/524778)
Branch Coverage 58.26% (134334/230575)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants